#pragma once

#include "698Frame.hpp"
#include "actionRequestApdu.hpp"
#include "actionResponseApdu.hpp"
#include "byteStream.hpp"
#include "deviceStat.hpp"
#include "getRequestNormalApdu.hpp"
#include "pointTable.hpp"
#include "securityRequestApdu.hpp"
#include "serverApdu.hpp"
#include "uart.hpp"
#include "uartParam.hpp"
#include <algorithm>
#include <string>

inline std::ostream &operator<<(std::ostream &out, const ServerApdu::DataValueType &d) {
    for (const auto &item : d) {
        out << "{\n\ttype : " << U32(item.first) << "\n";
        out << "\tvalue : " << item.second << "\n}";
    }
    return out;
}

class Device {
private:
    static inline uint8_t defaultClientAddr = 0x01;
    static inline uint8_t defaultCtrlCode = 0x43;

    using Action = std::function<void(Device *)>;
    using OnlineAction = std::vector<Action>;
    using OfflineAction = std::vector<Action>;

protected:
    std::size_t _id;
    std::string _name;
    std::string _uuid;
    std::string _type;
    std::string _profile;
    std::string _model;

    std::string _address;
    PointTable _pointTable;
    UartParam _param;
    OnlineAction _onlineAction;
    OfflineAction _offlineAction;
    DeviceStat _status;
    // std::shared_ptr<Uart> _uart;
public:
    using iterator = PointTable::iterator;
    using const_iterator = PointTable::const_iterator;

    iterator begin() { return _pointTable.begin(); }
    const_iterator begin() const { return _pointTable.begin(); }

    iterator end() { return _pointTable.end(); }
    const_iterator end() const { return _pointTable.end(); }

private:
    iterator _currentPos;

private:
    bool online() {
        if (isOffline()) {
            std::for_each(_onlineAction.begin(), _onlineAction.end(), [this](const Action &f) { f(this); });
        }
        _status = DeviceStat::DEVICE_STAT_ONLINE;
        return true;
    }

    bool offline() {
        if (isOnline()) {
            std::for_each(_offlineAction.begin(), _offlineAction.end(), [this](const Action &f) { f(this); });
        }
        _status = DeviceStat::DEVICE_STAT_OFFLINE;
        return false;
    }

public:
    ByteStream address(void) const { return ByteStream().fromHexString(_address).reverse(); }

    std::size_t id(void) const { return _id; }

    Device &setAddress(const std::string &a) {
        _address = a;
        return *this;
    }

    Device &setProfile(const std::string &profile) {
        _profile = profile;
        _pointTable.fromFile(profile);
        return *this;
    }

    Device &setParam(const UartParam &p) {
        _param = p;
        return *this;
    }

    bool isOnline(void) const { return _status == DeviceStat::DEVICE_STAT_ONLINE; }
    bool isOffline(void) const { return !isOnline(); }

    bool hasPoint(const std::string &name) { return _pointTable.findPoint(name) != nullptr; }
    std::any pointValue(const std::string &name) {
        auto point = _pointTable.findPoint(name);
        if (point) {
            return point->value();
        }
        return nullptr;
    }

    template <typename F, typename... Args>
    void registerReadCallback(F &&f, Args &&... args) {
        for (auto &point : _pointTable) {
            if (point.second.readAble()) {
                point.second.registerReadCallback(std::forward<F>(f), std::forward<Args>(args)...);
            }
        }
    }

    template <typename F, typename... Args>
    void registerOnlineCallback(F &&f, Args &&... args) { _onlineAction.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1)); }

    template <typename F, typename... Args>
    void registerOfflineCallback(F &&f, Args &&... args) { _offlineAction.emplace_back(std::bind(std::forward<F>(f), std::forward<Args>(args)..., std::placeholders::_1)); }

    Point *point(const std::string &name) { return _pointTable.findPoint(name); }

    bool readPoint(const std::string &name) {
        auto point = _pointTable.findPoint(name);
        if (!point) {
            std::cerr << "can't find point, point name --> " << name << std::endl;
            return false;
        }

        if (!point->readAble()) {
            std::cerr << "point can't read, please check operationCode, point name --> " << name << std::endl;
            return point->operatorFail();
        }

        ByteStream tx;
        if (point->isNormalRequest()) {
            tx = Dlt698Frame().buildRequest(defaultClientAddr, defaultCtrlCode, address(), GetRequestNormalApdu(point->oad()));
        } else if (point->isSecurityRequest()) {
            tx = Dlt698Frame().buildRequest(defaultClientAddr, defaultCtrlCode, address(), SecurityRequestApdu(GetRequestNormalApdu(point->oad())));
        } else {
            std::cerr << "not suppost operatorCode" << std::endl;
        }

        std::cout << "point name --> " << name << std::endl;
        std::cout << "tx --> " << tx << std::endl;
        auto rx = Dlt698Frame(Uart::transmit(_param, tx, point->waitTimeout()));
        std::cout << "rx --> " << rx.frame() << std::endl;
        auto apdu = rx.serverApdu();
        std::cout << "apdu --> " << apdu->apdu() << std::endl;

        if (!apdu->isGood()) {
            std::cerr << "bad 698 frame!!!" << std::endl;
            return offline() || point->operatorFail();
        }

        auto val = apdu->value();

        if (point->hasReadCallback() && !val.empty()) {
            point->readCallback(this, point, point->rawData(), val[0].second);
        }

        point->setData(val);
        std::cout << "data --> " << val << std::endl;

        return point->operatorSuccess() && online();
    }

    bool readNextPoint(void) {
        bool ret = false;
        if (_currentPos->second.readAble()) {
            ret = readPoint(_currentPos->first);
        }
        _currentPos++;
        _currentPos = std::find_if(_currentPos, _pointTable.end(), [](const std::pair<std::string, Point> &p) { return p.second.readAble(); });

        if (_currentPos == _pointTable.end()) {
            _currentPos = std::find_if(_pointTable.begin(), _pointTable.end(), [](const std::pair<std::string, Point> &p) { return p.second.readAble(); });
        }

        return ret;
    }

    bool writePoint(const std::string &name, const ByteStream &d) {
        auto point = _pointTable.findPoint(name);
        if (!point) {
            std::cerr << "can't find point, point name --> " << name << std::endl;
            return false;
        }

        if ((!point->actionAble()) && (!point->writeAble())) {
            std::cerr << "point can't operator or write, point name --> " << name << std::endl;
            return false;
        }

        ByteStream tx;
        if (point->actionAble()) {
            tx = Dlt698Frame().buildRequest(defaultClientAddr, defaultCtrlCode, address(), ActionRequestApdu(point->oad(), d));
        } else if (point->writeAble()) {
            // todo:
        } else {
            std::cerr << "can't write or operation, point name --> " << name << std::endl;
            return false;
        }

        std::cout << "point name --> " << name << std::endl;
        std::cout << "tx --> " << tx << std::endl;
        auto rx = Dlt698Frame(Uart::transmit(_param, tx, point->waitTimeout()));
        std::cout << "rx --> " << rx.frame() << std::endl;
        auto apdu = rx.serverApdu();
        std::cout << "apdu --> " << apdu->apdu() << std::endl;

        return apdu->isGood() ? (point->operatorSuccess()) : (point->operatorFail());
    }

    // bool operatorPoint(const std::string &name, const ByteStream &d) {
    //     auto point = _pointTable.findPoint(name);
    //     if (!point) {
    //         std::cerr << "can't find point, point name --> " << name << std::endl;
    //         return false;
    //     }

    //     if (!point->actionAble()) {
    //         std::cerr << "point can't operator, point name --> " << name << std::endl;
    //         return false;
    //     }

    //     return true;
    // }

    friend void from_json(const nlohmann::json &j, Device &d) {
        if (j.contains("id")) {
            j.at("id").get_to(d._id);
        }

        if (j.contains("name")) {
            j.at("name").get_to(d._name);
        }

        if (j.contains("uuid")) {
            j.at("uuid").get_to(d._uuid);
        }

        if (j.contains("type")) {
            j.at("type").get_to(d._type);
        }

        if (j.contains("model")) {
            j.at("model").get_to(d._model);
        }

        if (j.contains("param")) {
            if (j.at("param").contains("address")) {
                j.at("param").at("address").get_to(d._address);
            }

            if (j.at("param").contains("baudrate")) {
                d._param.setBaudrate(j.at("param").at("baudrate").get<int>());
            }

            if (j.at("param").contains("bytesize")) {
                d._param.setDatabits(j.at("param").at("bytesize").get<int>());
            }

            if (j.at("param").contains("stopbits")) {
                d._param.setStopbits(j.at("param").at("stopbits").get<int>());
            }

            if (j.at("param").contains("parity")) {
                d._param.setParity(j.at("param").at("parity").get<std::string>());
            }

            if (j.at("param").contains("port")) {
                d._param.setPort(j.at("param").at("port").get<std::string>());
            }
        }

        if (j.contains("profile")) {
            j.at("profile").get_to(d._profile);
            d._pointTable.fromFile(d._profile);
            d._currentPos = d._pointTable.begin();
        }

        d._status = DeviceStat::DEVICE_STAT_OFFLINE;
    }

    friend void to_json(nlohmann::json &j, const Device &d) {
        j = {{"name", d._name}, {"uuid", d._uuid}, {"id", d._id}, {"type", d._type}, {"model", d._model}, {"profile", d._profile}};
        j["param"]["address"] = d._address;
        j["param"]["baudrate"] = d._param.rawBaudrate();
        j["param"]["bytesize"] = d._param.databits();
        j["param"]["stopbits"] = d._param.stopbits();
        j["param"]["parity"] = std::string(1, d._param.parity());
        j["param"]["port"] = d._param.port();
    }
};
